<?php
/**
 * 排行榜
 * 1.可以设置排行榜的过期时间
 * 2.积分可以自增或者重置
 * 3.获取指定用户的排行
 *
 * 额外情况
 * 处理同分,按照时间顺序排序
 * 先进入的排行靠前
 * 一年秒数 31536000 8位
 * 2024年时间戳 1729715643 10位 1.72 * 10^9
 * 时间戳 1*10^10 可以到达2286年
 * 时间戳的最大差值 1*10^10-1-时间戳  1*10^10-1-1729715643=8270284356 后面只会越来越小
 * 浮点数最大有效整数2^53 16位十进制 9007199254740992.0
 * 是可以存比2^53大的数,只不过会丢失精度
 *
 *
 * 方案1 socre 使用整数  高位存分数 低位存间戳差值 二进制方式位移
 *    有效位是52位位数+隐藏为1
 *    低位33位存时间戳差值 最大值8589934592-1 可以用了 高位剩下19+1位,保险只能存6位分数 最大1048576=1.04*10^5=2^20-1  7位数
 *    低位34位存时间戳差值                          高位剩下18+1位,保险只能存5位分数 最大52488=5.24*10^5=2^19-1  6位数
 * 方案2 socre 使用整数 数字方式-十进制方式 前部分存分数 后半部分存时间戳的最大差值
 *      十进制 高位存分数 分数*1*10^10
 *      十进制 低位存时间戳差 1*10^10-1-时间戳
 *      时间戳10位 高6位存分数
 *      9007199254740992.0  高6位最大存900719
 * 方案3 socre 使用浮点数 整数部分存分数 小数部分存时间戳的差值
 *      时间戳10位，精度在10位的小数点的2^-33=1.16*10^-10
 *      为了实现精确到1s,要求能存精度在10位的小数点,指数是52-33=19 整数位5-6位左右 2^20=1.04*10^7
 *      分数特别大导致失去精度
 * 方案4 时间单独存到hash里, 排序时 排序两个字段
 *      适合场景   没有大量同分的情况
 *      不适合场景 大量同分的情况,或者分数都是一个小区间 拉不开差距的情况
 * 方案 1,2,3 原理相同
 *  同分情况下,存大的分数,时间戳差值会失去精度,但是如果是现实环境,越大的分数,同分的情况越少,适合现实场景
 *  可以存大分数,即使时间戳失去精度
 *  也可以存 大量同分的情况
 *
 * 只有一种情况需要 方案4 分数差值特别大,分数也能达到特别大值,没有大量同分的情况
 */

/**
 * 方案1
 * 按照时间 同分情况 先入排序更高
 * 时间戳最大可以到 2286年
 * 低33位 存时间戳差值
 * 高19+1 存分数 所以分数左移33位
 * 分数只有20位二进制 最大值为1048576  2^20-1
 * 比2^20-1大的值可以存储,就是同分情况,会失去时间精度
 * Class BitRank
 */
include_once "rank.php";

class BitRank extends rank
{
    const TIME_BITS = 33;                //位移位数
    const TIME_BITS_MAX = 0x1FFFFFFFF;   //33位的二进制最大数 8589934591=8.58*10^9 就是时间搓差值的最大值
    const TIME_MAX = 10000000000;        //1*10^10 TIME_MAX-time()=时间搓差值 所以时间戳最大=TIME_MAX 到2286年


    public function set($member, $score, $incr = false, $time = null)
    {
        $time = $time ?? time();
        $score = $score << self::TIME_BITS;
        if ($incr) {
            //先增加分数 再修改时间戳差值 允许并发增加分数,不允许并发1秒处理时间差 重置时间差
            $newScore = parent::set($member, $score, $incr);
            if(!$this->lock()) {
                return 'retime';
            }
            $timeDiff = (int)$newScore & self::TIME_BITS_MAX;
            $timeDiff =  self::TIME_MAX - 1 - $time  - $timeDiff;
            return Rank::set($member, $timeDiff, $incr);
        }
        $timeDiff = self::TIME_MAX - 1 - $time;
        $score +=  $timeDiff;
        //var_dump($timeDiff, $score);
        return Rank::set($member, $score, $incr);
    }

    public function getRankList($count = 100)
    {
        $list = parent::getRankList($count);
        foreach ($list as $k => $v) {
            $list[$k] = $this->getRealScore($v);
        }
        return $list;
    }

    public function getInfo($member)
    {
        $info = parent::getInfo($member);
        $info['score'] = $this->getRealScore($info['score']);
    }

    /**
     * 生成测试排行榜
     */
    function generate($num = 100, $shuffle = true, $step = 1, $init = 1)
    {
        $key = $this->key;
        $redis = $this->redis;
        $expire = $this->expire;
        $arr = range('a', 'z');
        $score = $init-$step;
        for($i = 1;$i< $num;$i++) {
           // $redis->zAdd($key, rand(200,300), $i);
            $member = $arr[rand(0,25)] . $i;
            $score = $shuffle ?  rand(200,300) : $score + $step;
            $this->set($member, $score);
        }
        $redis->expire($key, $expire);
    }

    public function getRealScore($score)
    {
        return intval($score >> self::TIME_BITS);
    }

    public function getTimeDiff($time)
    {
        return self::TIME_MAX - 1 - $time;
    }

    public function lock($ttl = 1)
    {
        $lockKey = 'rank_lock:' . $this->key;
        if ($ttl) return $this->redis->set('rank_lock:' . $this->key, 1, ['NX', 'EX'=>$ttl]);
        $this->redis->del($lockKey);

    }
}

/**
 * Class DecTimeRank
 * 方案二
 *  按照时间 同分情况 先入排序更高
 *  使用整数 数字方式-十进制方式 前部分存分数 后半部分存时间戳的最大差值
 *  十进制 高位存分数 分数*1*10^10
 *  十进制 低位存时间戳差 1*10^10-1-时间戳
 *  浮点数 最大有效 整数2^53 16位十进制
 *  时间戳10位 高6位存分数
 *  时间戳最大到1*10^10 2286年
 *  分数最大值为 900719
 *  超过900719可以存储,但是时间部分会丢失精度,分数部分在高位,精度从低位开始失去,所以分数还是可以取到的
 * Class BitRank
 */
class DecTimeRank extends BitRank
{
    public function set($member, $score, $incr = false, $time = null)
    {
        !$time && $time = time();
        $score = $score * self::TIME_MAX;
        if ($incr) {
            //先增加分数 再修改时间戳差值 允许并发增加分数,不允许并发1秒处理时间差 重置时间差
            $newScore = Rank::set($member, $score, $incr);
            $lock = $this->lock();
            if(!$lock) {
                return 'retime lock';
            }
            $timeDiff = $this->getTimeDiffByScore($newScore);
            $newTimeDiff = $this->getTimeDiff($time);
            $timeIncr =  $newTimeDiff  - $timeDiff;
            return Rank::set($member, $timeIncr, $incr);
        }
        $timeDiff = $this->getTimeDiff($time);
        $score = $score + $timeDiff;
        return Rank::set($member, $score, $incr);
    }

    public function getRankList($count = 100)
    {
        $list = Rank::getRankList($count);
        foreach ($list as $k => $v) {
            $list[$k] = $this->getRealScore($v);
        }
        return $list;
    }

    public function getInfo($member)
    {
        $info = parent::getInfo($member);
        $info['score'] = $this->getRealScore($info['score']);
    }

    public function getRealScore($score)
    {
        return intval($score / self::TIME_MAX);
    }

    public function getTimeDiff($time)
    {
        return self::TIME_MAX - 1 - $time;
    }

    public function getTimeDiffByScore($RedisScore)
    {
        return substr((string)$RedisScore, -10);
    }
}

/**
 * Class FloatRank
 * 方案三
 *  按照时间 同分情况 先入排序更高
 *  socre 使用浮点数 整数部分存分数 小数部分存时间戳的差值
 *  时间戳10位，精度在10位的小数点的2^-33=1.16*10^-10
 *  时间戳最大到 1*10^10 2286年
 *  为了实现精确到1s,要求能存精度在10位的小数,指数是52-33=19 整数位5-6位左右 2^20-1=1048576
 *  分数也特别大导致失去精度
 *  分数最大值为 900719  比9*10^5大的值可以存储,就是同分情况,会失去时间精度
 */
class TimeRank extends BitRank
{
    public function set($member, $score, $incr = false, $time = null)
    {
        !$time && $time = time();
        if ($incr) {
            //先增加分数 再修改时间戳差值 允许并发增加分数, 不允许并发1秒处理时间差 因为每次都是重置时间差
            $newScore = Rank::set($member, $score, $incr);
            $lock = $this->lock();
            if(!$lock) {
                return 'retime lock';
            }
            $timeDiff = $this->getTimeDiffByScore($newScore);
            $newTimeDiff = $this->getTimeDiff($time);
            $timeIncr =  round($newTimeDiff  - $timeDiff, 9);
            //var_dump($timeDiff, $newTimeDiff, $timeIncr,$score, $newScore);
            return Rank::set($member, $timeIncr, $incr);
        }
        $timeDiff = $this->getTimeDiff($time);
        $score = $score + $timeDiff;
        return Rank::set($member, $score, $incr);
    }

    public function getRankList($count = 100)
    {
        $list = Rank::getRankList($count);
        foreach ($list as $k => $v) {
            $list[$k] = $this->getRealScore($v);
        }
        return $list;
    }

    public function getInfo($member)
    {
        $info = parent::getInfo($member);
        $info['score'] = $this->getRealScore($info['score']);
    }

    public function getRealScore($score)
    {
        return intval($score);
    }

    public function getTimeDiff($time)
    {
        return '0.' . abs(self::TIME_MAX - 1 - $time);
    }

    public function getTimeDiffByScore($RedisScore)
    {
        //不能取最后10位,因为有可能尾部小数的0被移除了
        //return '0.' . substr((string)$RedisScore, -10);
        return '0' . strstr($RedisScore, '.');
    }
}

/**
 * Class MulitRank
 * 方案四
 * 时间单独存到hash里, 排序时 排序两个字段
 * 适合场景   没有大量同分的情况
 * 不适合场景 大量同分的情况,或者分数都是一个小区间 拉不开差距的情况
 */
class MulitRank extends Rank
{
    public function set($member, $score, $incr = false, $time = null)
    {
        !$time && $time = time();
        $ret = Rank::set($member, $score, $incr);
        $this->setTime($member, $time);
        return $ret;
    }

    public function setTime($member, $time)
    {
        $key = $this->getTimeKey();
       return  $this->redis->hset($key, $member, $time);
    }

    public function getRankList($count = 100)
    {
        $list = Rank::getRankList($count+20);
        $members = array_keys($list);
        $memberTimes = $this->getTimes($members);
        $newList = [];
        foreach ($list as $k => $v) {
            $times[$k] = $memberTimes[$k] ?? null;
            $newList[$k] = ['score' => $v, 'member' => $k];
        }
        //数字键名会被重置键名,如果member是数字或者数字字符串都将被重置
        array_multisort($list, SORT_DESC, $times, SORT_ASC, $newList);
        $list = array_column($newList, 'score', 'member');
        return $list;
    }

    /**
     * 同分排名情况处理
     */
    public function getInfo($member)
    {
        $info = parent::getInfo($member);
        $score = $info['score'];
        $sameMembers = $this->redis->zrevRangeByScore($this->key, $score, $score);
        if (count($sameMembers) > 1) {
            $initRank = $this->redis->zRevRank($this->key, $sameMembers[0]);
            $times = $this->getTimes($sameMembers);
            asort($times);
            $index = array_search($member, array_keys($times));
            $info['rank'] = $initRank + $index;
            $info['time'] = $times[$member] ?? 0;
        }
        return $info;
    }

    public function getTimes($members)
    {
        $key = $this->getTimeKey();
        return $this->redis->hMGet($key, $members);
    }

    public function getTimeKey()
    {
        return $this->key . ':time';
    }
}